package io.kiki.stack.http.feign.hc5;

import feign.Client;
import feign.Request;
import feign.Response;
import feign.Util;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.Configurable;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.GzipCompressingEntity;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.net.URIBuilder;
import org.apache.hc.core5.net.URLEncodedUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.*;

import static feign.Util.UTF_8;
import static feign.Util.enumForName;

/**
 * This module directs Feign's http requests to Apache's
 * <a href="https://hc.apache.org/httpcomponents-client-5.0.x/index.html">HttpClient 5</a>. Ex.
 *
 * <pre>
 * GitHub github = Feign.builder().client(new ApacheHttp5Client()).target(GitHub.class,
 * "https://api.github.com");
 */
/*
 */
public final class ApacheHttp5Client implements Client {
    private static final String ACCEPT_HEADER_NAME = "Accept";

    private final HttpClient client;

    public ApacheHttp5Client() {
        this(HttpClientBuilder.create().build());
    }

    public ApacheHttp5Client(HttpClient client) {
        this.client = client;
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        ClassicHttpRequest httpUriRequest;
        try {
            httpUriRequest = toClassicHttpRequest(request, options);
        } catch (final URISyntaxException e) {
            throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e);
        }
        final HttpHost target = HttpHost.create(URI.create(request.url()));
        final HttpClientContext context = configureTimeoutsAndRedirection(options);

        final ClassicHttpResponse httpResponse = (ClassicHttpResponse) client.execute(target, httpUriRequest, context);
        return toFeignResponse(httpResponse, request);
    }

    protected HttpClientContext configureTimeoutsAndRedirection(Request.Options options) {
        final HttpClientContext context = new HttpClientContext();
        // per request timeouts
        final RequestConfig requestConfig = (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig()) : RequestConfig.custom()).setConnectTimeout(options.connectTimeout(), options.connectTimeoutUnit()).setResponseTimeout(options.readTimeout(), options.readTimeoutUnit()).setRedirectsEnabled(options.isFollowRedirects()).build();
        context.setRequestConfig(requestConfig);
        return context;
    }

    ClassicHttpRequest toClassicHttpRequest(Request request, Request.Options options) throws URISyntaxException {
        final ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.create(request.httpMethod().name());

        final URI uri = new URIBuilder(request.url()).build();

        requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath());

        // request query params
        final List<NameValuePair> queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset());
        for (final NameValuePair queryParam : queryParams) {
            requestBuilder.addParameter(queryParam);
        }

        // request headers
        boolean hasAcceptHeader = false;
        boolean isGzip = false;
        for (final Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
            final String headerName = headerEntry.getKey();
            if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
                hasAcceptHeader = true;
            }

            if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) {
                // The 'Content-Length' header is always set by the Apache client and it
                // doesn't like us to set it as well.
                continue;
            }
            if (headerName.equalsIgnoreCase(Util.CONTENT_ENCODING)) {
                isGzip = headerEntry.getValue().stream().anyMatch(Util.ENCODING_GZIP::equalsIgnoreCase);
                boolean isDeflate = headerEntry.getValue().stream().anyMatch(Util.ENCODING_DEFLATE::equalsIgnoreCase);
                if (isDeflate) {
                    // DeflateCompressingEntity not available in hc5 yet
                    throw new IllegalArgumentException("Deflate Content-Encoding is not supported by feign-hc5");
                }
            }
            for (final String headerValue : headerEntry.getValue()) {
                requestBuilder.addHeader(headerName, headerValue);
            }
        }
        // some servers choke on the default accept string, so we'll set it to anything
        if (!hasAcceptHeader) {
            requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*");
        }

        // request body
        // final Body requestBody = request.requestBody();
        byte[] data = request.body();
        if (data != null) {
            HttpEntity entity;
            if (request.isBinary()) {
                entity = new ByteArrayEntity(data, null);
            } else {
                final ContentType contentType = getContentType(request);
                String content;
                if (request.charset() != null) {
                    content = new String(data, request.charset());
                } else {
                    content = new String(data);
                }
                entity = new StringEntity(content, contentType);
            }
            if (isGzip) {
                entity = new GzipCompressingEntity(entity);
            }
            requestBuilder.setEntity(entity);
        } else {
            requestBuilder.setEntity(new ByteArrayEntity(new byte[0], null));
        }

        return requestBuilder.build();
    }

    private ContentType getContentType(Request request) {
        ContentType contentType = null;
        for (final Map.Entry<String, Collection<String>> entry : request.headers().entrySet()) {
            if (entry.getKey().equalsIgnoreCase("Content-Type")) {
                final Collection<String> values = entry.getValue();
                if (values != null && !values.isEmpty()) {
                    contentType = ContentType.parse(values.iterator().next());
                    if (contentType.getCharset() == null) {
                        contentType = contentType.withCharset(request.charset());
                    }
                    break;
                }
            }
        }
        return contentType;
    }

    Response toFeignResponse(ClassicHttpResponse httpResponse, Request request) throws IOException {
        final int statusCode = httpResponse.getCode();

        final String reason = httpResponse.getReasonPhrase();

        final Map<String, Collection<String>> headers = new HashMap<>();
        for (final Header header : httpResponse.getHeaders()) {
            final String name = header.getName();
            final String value = header.getValue();

            Collection<String> headerValues = headers.get(name);
            if (headerValues == null) {
                headerValues = new ArrayList<>();
                headers.put(name, headerValues);
            }
            headerValues.add(value);
        }

        return Response.builder().protocolVersion(enumForName(Request.ProtocolVersion.class, httpResponse.getVersion().format())).status(statusCode).reason(reason).headers(headers).request(request).body(toFeignBody(httpResponse)).build();
    }

    Response.Body toFeignBody(ClassicHttpResponse httpResponse) {
        final HttpEntity entity = httpResponse.getEntity();
        if (entity == null) {
            return null;
        }
        return new Response.Body() {

            @Override
            public Integer length() {
                return entity.getContentLength() >= 0 && entity.getContentLength() <= Integer.MAX_VALUE ? (int) entity.getContentLength() : null;
            }

            @Override
            public boolean isRepeatable() {
                return entity.isRepeatable();
            }

            @Override
            public InputStream asInputStream() throws IOException {
                return entity.getContent();
            }

            @Override
            public Reader asReader() throws IOException {
                return new InputStreamReader(asInputStream(), UTF_8);
            }

            @Override
            public Reader asReader(Charset charset) throws IOException {
                Util.checkNotNull(charset, "charset should not be null");
                return new InputStreamReader(asInputStream(), charset);
            }

            @Override
            public void close() throws IOException {
                try {
                    EntityUtils.consume(entity);
                } finally {
                    httpResponse.close();
                }
            }
        };
    }
}
